"""
登录工厂模式 - 统一管理多种登录方式
支持：账号密码登录、钉钉登录（可扩展：微信、支付宝等）
"""
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any
from sqlalchemy.orm import Session
from datetime import datetime
import logging
from Models.login.models import User, LoginLog, LoginType
from utils import JWTUtils, RedisUtils, DingTalkUtils
from utils.dingtalk_utils import OAuthError

logger = logging.getLogger(__name__)


class LoginStrategy(ABC):
    """登录策略抽象基类"""
    
    @abstractmethod
    async def authenticate(self, request_data: Dict[str, Any], request, db: Session) -> Dict[str, Any]:
        """
        执行认证逻辑
        :param request_data: 请求数据
        :param request: FastAPI Request对象
        :param db: 数据库会话
        :return: {"user": User对象, "log_data": 日志数据字典} 或 {"error": "错误信息", "log_data": {...}}
        """
        pass
    
    @abstractmethod
    def get_login_type(self) -> LoginType:
        """获取登录类型"""
        pass


class PasswordLoginStrategy(LoginStrategy):
    """账号密码登录策略 - 重写版本"""
    
    def get_login_type(self) -> LoginType:
        return LoginType.PASSWORD
    
    async def authenticate(self, request_data: Dict[str, Any], request, db: Session) -> Dict[str, Any]:
        """
        账号密码登录认证
        request_data应包含: {"username": str, "password": str}
        """
        from starlette.concurrency import run_in_threadpool
        
        username = request_data.get("username", "").strip()
        password = request_data.get("password", "")
        
        # 参数验证
        if not username:
            return {
                "error": "用户名/邮箱/手机号不能为空",
                "log_data": {
                    "login_type": LoginType.PASSWORD,
                    "ip_address": request.client.host if request.client else None,
                    "user_agent": request.headers.get("user-agent"),
                }
            }
        
        if not password:
            return {
                "error": "密码不能为空",
                "log_data": {
                    "login_type": LoginType.PASSWORD,
                    "ip_address": request.client.host if request.client else None,
                    "user_agent": request.headers.get("user-agent"),
                }
            }
        
        # 确保密码是字符串类型
        if not isinstance(password, str):
            password = str(password)
        
        log_data = {
            "login_type": LoginType.PASSWORD,
            "ip_address": request.client.host if request.client else None,
            "user_agent": request.headers.get("user-agent"),
        }
        
        # 查找用户（支持用户名、邮箱、手机号登录）
        def find_user():
            return db.query(User).filter(
                (User.username == username) |
                (User.email == username) |
                (User.phone == username)
            ).first()
        
        user = await run_in_threadpool(find_user)
        
        # 验证用户是否存在
        if not user:
            return {
                "error": "用户不存在",
                "user": None,
                "log_data": log_data
            }
        
        # 检查用户是否有密码（账号密码登录必须设置密码）
        if not user.password_hash:
            return {
                "error": "该账号未设置密码，请使用其他登录方式",
                "user": user,
                "log_data": log_data
            }
        
        # 验证密码
        try:
            # 使用JWT工具验证密码（内部已处理密码长度限制）
            password_ok = await run_in_threadpool(
                JWTUtils.verify_password,
                password,
                user.password_hash
            )
            
            if not password_ok:
                return {
                    "error": "密码错误",
                    "user": user,
                    "log_data": log_data
                }
            
        except Exception as e:
            logger.error(f"密码验证异常: {e}", exc_info=True)
            return {
                "error": "密码验证失败，请稍后重试",
                "user": user,
                "log_data": log_data
            }
        
        # 更新最后登录时间
        try:
            def update_last_login():
                user.last_login = datetime.now()
                db.commit()
            await run_in_threadpool(update_last_login)
        except Exception as e:
            logger.warning(f"更新最后登录时间失败: {e}")
        
        return {
            "user": user,
            "log_data": log_data
        }


class DingTalkLoginStrategy(LoginStrategy):
    """钉钉登录策略"""
    
    def get_login_type(self) -> LoginType:
        return LoginType.DINGTALK
    
    async def authenticate(self, request_data: Dict[str, Any], request, db: Session) -> Dict[str, Any]:
        """
        钉钉登录认证
        request_data应包含: {"code": str} (OAuth授权码)
        """
        from starlette.concurrency import run_in_threadpool
        
        auth_code = request_data.get("code")
        if not auth_code:
            return {
                "error": "授权码不能为空",
                "log_data": {
                    "login_type": LoginType.DINGTALK,
                    "ip_address": request.client.host if request.client else None,
                    "user_agent": request.headers.get("user-agent"),
                }
            }
        
        # 通过授权码获取用户信息
        try:
            user_info = await DingTalkUtils.get_user_info_by_code(auth_code)
        except OAuthError as e:
            return {
                "error": str(e),
                "log_data": {
                    "login_type": LoginType.DINGTALK,
                    "ip_address": request.client.host if request.client else None,
                    "user_agent": request.headers.get("user-agent"),
                }
            }
        
        if not user_info:
            return {
                "error": "获取钉钉用户信息失败",
                "log_data": {
                    "login_type": LoginType.DINGTALK,
                    "ip_address": request.client.host if request.client else None,
                    "user_agent": request.headers.get("user-agent"),
                }
            }
        
        log_data = {
            "login_type": LoginType.DINGTALK,
            "ip_address": request.client.host if request.client else None,
            "user_agent": request.headers.get("user-agent"),
        }
        
        # OAuth 2.0返回的字段
        userid = user_info.get("userid") or user_info.get("openId") or user_info.get("nick")
        unionid = user_info.get("unionid") or user_info.get("unionId")
        nickname = user_info.get("nick") or user_info.get("name") or userid
        avatar = user_info.get("avatarUrl") or user_info.get("avatar")
        mobile = user_info.get("mobile")
        email = user_info.get("email")
        
        if not userid:
            return {
                "error": "钉钉用户ID获取失败",
                "log_data": log_data
            }
        
        # 查找或创建用户（使用unionid作为唯一标识更合适）
        def find_or_create_user():
            user = None
            if unionid:
                user = db.query(User).filter(User.dingtalk_unionid == unionid).first()
            if not user and userid:
                user = db.query(User).filter(User.dingtalk_userid == userid).first()
            # 如果通过钉钉ID找不到用户，但手机号已存在，则关联到现有用户
            if not user and mobile:
                user = db.query(User).filter(User.phone == mobile).first()
            
            if not user:
                # 创建新用户
                username = f"dingtalk_{userid[:20]}"  # 限制长度
                user = User(
                    username=username,
                    nickname=nickname,
                    avatar=avatar,
                    email=email,
                    phone=mobile,
                    dingtalk_userid=userid,
                    dingtalk_unionid=unionid,
                )
                db.add(user)
                db.commit()
                db.refresh(user)
            else:
                # 更新用户信息
                user.nickname = nickname or user.nickname
                user.avatar = avatar or user.avatar
                user.email = email or user.email
                user.phone = mobile or user.phone
                user.dingtalk_userid = userid or user.dingtalk_userid
                user.dingtalk_unionid = unionid or user.dingtalk_unionid
                user.last_login = datetime.now()
                db.commit()
                db.refresh(user)
            return user
        
        user = await run_in_threadpool(find_or_create_user)
        
        return {
            "user": user,
            "log_data": log_data
        }


class LoginFactory:
    """登录策略工厂类"""
    
    _strategies: Dict[LoginType, type] = {
        LoginType.PASSWORD: PasswordLoginStrategy,
        LoginType.DINGTALK: DingTalkLoginStrategy,
    }
    
    @classmethod
    def create_strategy(cls, login_type: LoginType) -> LoginStrategy:
        """
        创建登录策略实例
        :param login_type: 登录类型
        :return: 登录策略实例
        """
        strategy_class = cls._strategies.get(login_type)
        if not strategy_class:
            raise ValueError(f"不支持的登录类型: {login_type}")
        return strategy_class()
    
    @classmethod
    def register_strategy(cls, login_type: LoginType, strategy_class: type):
        """
        注册新的登录策略（扩展性）
        :param login_type: 登录类型
        :param strategy_class: 策略类
        """
        if not issubclass(strategy_class, LoginStrategy):
            raise TypeError(f"{strategy_class} 必须继承自 LoginStrategy")
        cls._strategies[login_type] = strategy_class
    
    @classmethod
    def get_available_types(cls) -> list:
        """获取所有可用的登录类型"""
        return list(cls._strategies.keys())


class LoginService:
    """登录服务类 - 统一处理登录流程"""
    
    def __init__(self, strategy: LoginStrategy):
        self.strategy = strategy
    
    async def login(
        self, 
        request_data: Dict[str, Any], 
        request, 
        db: Session
    ) -> Dict[str, Any]:
        """
        执行登录流程
        :param request_data: 请求数据
        :param request: FastAPI Request对象
        :param db: 数据库会话
        :return: {"access_token": str, "user_info": dict} 或抛出异常
        """
        from starlette.concurrency import run_in_threadpool
        from fastapi import HTTPException, status
        
        # 1. 执行认证
        auth_result = await self.strategy.authenticate(request_data, request, db)
        
        if auth_result.get("error"):
            # 记录失败日志
            log_data = auth_result.get("log_data", {})
            user = auth_result.get("user")
            user_id = user.id if user else 0
            
            # 确保 login_type 存在
            if "login_type" not in log_data:
                log_data["login_type"] = self.strategy.get_login_type()
            
            try:
                login_log = LoginLog(
                    user_id=user_id,
                    is_success=False,
                    error_message=auth_result.get("error"),
                    **log_data
                )
                await run_in_threadpool(lambda: (db.add(login_log), db.commit()))
            except Exception as e:
                # 记录日志失败不影响登录流程
                logger.error(f"记录登录日志失败: {e}", exc_info=True)
            
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=auth_result.get("error")
            )
        
        user = auth_result.get("user")
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="认证失败"
            )
        
        # 2. 生成JWT令牌
        access_token = JWTUtils.create_access_token(
            data={
                "user_id": user.id,
                "username": user.username,
                "login_type": self.strategy.get_login_type().value
            }
        )
        
        # 3. 将登录态写入Redis
        try:
            await RedisUtils.save_token(
                token=access_token,
                user_id=user.id,
                payload={
                    "username": user.username,
                    "login_type": self.strategy.get_login_type().value,
                },
            )
        except Exception as e:
            # Redis 连接失败不影响登录流程，但记录日志
            logger.warning(f"保存Token到Redis失败: {e}，但登录继续")
        
        # 4. 记录成功登录日志
        log_data = auth_result.get("log_data", {})
        
        # 确保 login_type 存在
        if "login_type" not in log_data:
            log_data["login_type"] = self.strategy.get_login_type()
        
        try:
            login_log = LoginLog(
                user_id=user.id,
                is_success=True,
                **log_data
            )
            await run_in_threadpool(lambda: (db.add(login_log), db.commit()))
        except Exception as e:
            # 记录日志失败不影响登录流程
            logger.error(f"记录登录日志失败: {e}", exc_info=True)
        
        # 获取角色代码
        role_code = None
        if user.role_id:
            from Models.login.models import Role
            role = await run_in_threadpool(lambda: db.query(Role).filter(Role.id == user.role_id).first())
            if role:
                role_code = role.code
        
        # 5. 返回结果
        return {
            "access_token": access_token,
            "token_type": "bearer",
            "user_info": {
                "id": user.id,
                "username": user.username,
                "email": user.email,
                "phone": user.phone,
                "nickname": user.nickname,
                "avatar": user.avatar,
                "role_id": user.role_id,
                "role_code": role_code,
                "is_verified": user.is_verified
            }
        }

